Skip to content

Conversation

@ololoken
Copy link

@ololoken ololoken commented Jun 5, 2025

I'm working on wasm port.
Demo is available here https://turch.in/rttr/index.html
image
Right now it's not playable. As emscripten do not support socket listen in web environment.
And I need good advice: how do you (settlers freaks) see implementation of local connection without sockets?

Copy link
Member

@Flamefire Flamefire left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting approach. I added some comments on the changes if you want to continue with this.

Of course I need to ask for the motivation of this. Besides it clearly being a nice experiment on current capabilities.
E.g. I notice that it is already quite sluggish in the main menu. So why not using the regular builds given that we have performance issues for large maps with that already?

And I need good advice: how do you (settlers freaks) see implementation of local connection without sockets?

I think this would be an incredible effort possibly making the (regular) code much less maintainable. For now we can consider all games non-local/multiplayer for the purpose of the core logic.
Decoupling this means we would have 2 almost completely separate control flows that would need testing and being unable to detect and debug multiplayer issues using the regular flow.
I'd rather not do that.
Is there really no other way?

CMakeLists.txt Outdated
set(rttrContribBoostDir ${CMAKE_CURRENT_SOURCE_DIR}/contrib/boost)
if(EXISTS ${rttrContribBoostDir} AND IS_DIRECTORY ${rttrContribBoostDir})
set(BOOST_ROOT ${rttrContribBoostDir} CACHE PATH "Path to find boost at")
if (${CMAKE_SYSTEM_NAME} MATCHES "Emscripten")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Common pitfall: This could get double-extended. Just omit the dollar sign

Suggested change
if (${CMAKE_SYSTEM_NAME} MATCHES "Emscripten")
if (CMAKE_SYSTEM_NAME MATCHES "Emscripten")

if(EXISTS ${rttrContribBoostDir} AND IS_DIRECTORY ${rttrContribBoostDir})
set(BOOST_ROOT ${rttrContribBoostDir} CACHE PATH "Path to find boost at")
if (${CMAKE_SYSTEM_NAME} MATCHES "Emscripten")
set(Boost_INCLUDE_DIR "${rttrContribBoostDir}/include" CACHE PATH "Path to find boost at")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not required: BOOST_ROOT is enough. Possibly should be replaced by Boost_ROOT though.

option(RTTR_USE_SYSTEM_BOOST_NOWIDE "Use system installed Boost.Nowide. Fails if not found!" "${RTTR_USE_SYSTEM_LIBS}")

if (${CMAKE_SYSTEM_NAME} MATCHES "Emscripten")
set(RTTR_USE_SYSTEM_BOOST_NOWIDE ON)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verify indents

Suggested change
set(RTTR_USE_SYSTEM_BOOST_NOWIDE ON)
set(RTTR_USE_SYSTEM_BOOST_NOWIDE ON)

Comment on lines 54 to 55
if (${CMAKE_SYSTEM_NAME} MATCHES "Emscripten")
else ()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid those here and down, also correct indent for next line

Suggested change
if (${CMAKE_SYSTEM_NAME} MATCHES "Emscripten")
else ()
if (NOT CMAKE_SYSTEM_NAME MATCHES "Emscripten")

}

const char* GetDriverName()
const char* GetAudioDriverName()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This breaks the interface, see

auto GetDriverName = dll.get<GetDriverName_t>("GetDriverName");

return Initialize();
}

bool VideoDriverWrapper::LoadDriver()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See suggestion for Audiodriver

renderer_ = std::make_unique<DummyRenderer>();
if(!renderer_->initOpenGL(videodriver->GetLoaderFunction()))
return false;
#ifndef __EMSCRIPTEN__
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For if-else rather use the non-inverted, i.e. start with #ifdef __EMSCRIPTEN__

bool OpenGLRenderer::initOpenGL(OpenGL_Loader_Proc loader)
{
#if RTTR_OGL_ES
#if defined(RTTR_OGL_ES)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be set, shouldn't it? Using #if instead of #ifdef allows us to detect misspellings so we should keep it

#if RTTR_OGL_ES
#if defined(RTTR_OGL_ES)
return gladLoadGLES2Loader(loader) != 0;
#elif __EMSCRIPTEN__
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be ifdef similar to other places? If so I guess it is easier to put at the top as ifdef and elif RTTR_OGL_ES here

#include "SDL/SDL.h"
#include "SDL/SDL_opengl.h"
#else
#include <glad/glad.h>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't glad work with emscripten? This is repeated so often even though there is no other change so I'd think it should work using the same method as glad can handle e.g. OpenGL ES already

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It can work, at least latest versions claims support of emscripten. I just didn't find good reason for myself to keep glad.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well I'm asking because of maintainability: We are using glad to support all platforms without additional ifdefs.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand, will check glad for option to migrate back to glad, right now there is problem runtime gl function resolution.
BTW about networking I see the first packet from server to client is ping 0x01, 0x02, 0x01, 0, 0, 0, right? How does handshake routine look like?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ololoken
Copy link
Author

ololoken commented Jun 6, 2025

@Flamefire well, motivation is to get original fill and look. First I ported widelands, playable version is here https://turch.in/widelands/ . But it's a bit different game... Besides, it's cool to have version of the game which doesn't require download and install, it just works right now and here in browser.
Yes there are performance issues, wasm is executed in 32bit vm with jit, but I think it's not a big problem as machines gets faster(slower than before, but steady) and wasm vm soon may get fat performance boost with upcoming updates.
I will go through comments later and add fixes. First I want to consider workaround for network problem. I agree that adding of custom logic for local game is bad idea in matter of maintenatability. There few options I can try: it's time to write own websocket-tcp/udp proxy, hack libwebsocket.js to make virtual peer connections for localhost.

@ololoken
Copy link
Author

ololoken commented Jun 8, 2025

Well, experiment with hacking of libsock.js was successful + I had to slightly modify game network code: recv messages in complete chunks, without waiting for any additional blocks.
image
But @Flamefire you are right, ingame performance is too poor even with aggressive optimisations. Do you have any ideas how to fix that?

@Farmer-Markus
Copy link
Contributor

It seems like you have the same problem with the graphics/shadows as my Android port of rttr. Do you know why this happens? Would help me a lot :D

@Flamefire
Copy link
Member

But @Flamefire you are right, ingame performance is too poor even with aggressive optimisations. Do you have any ideas how to fix that?

TBH: The core code isn't exactly optimized for performance. There are a lot of indirections and virtual calls leading to pointer-chasing and poor branch prediction.
But the code has to work at all and be easy enough to follow to verify it is always in sync for all players. I.e. the simulation must be in lock-step which was the main focus with performance being "good enough" except for large maps and road networks where the pathfinding starts to be a bottleneck.

I don't think there is an easy way to fix this without rewriting a lot of the code which then might reintroduce especially all the async bugs we fixed over the years. Although there are some targeted optimization opportunities like one recently fixed in the pathfinding code: #1734

@ololoken
Copy link
Author

ololoken commented Jun 9, 2025

It seems like you have the same problem with the graphics/shadows as my Android port of rttr. Do you know why this happens? Would help me a lot :D

There must something related to textures format/internal format. I'm looking at TerrainRenderer and something tells me that 99% of its code can be done by shaders.

@Flamefire
Copy link
Member

Flamefire commented Jun 9, 2025

There must something related to textures format/internal format. I'm looking at TerrainRenderer and something tells me that 99% of its code can be done by shaders.

I started with introducing shaders a long time ago but never got around finishing it. My first point was about handling player textures by the shader instead of the bitmap class. I.e. have the shader combine the main texture and color the masked overlay.

And yes the TerrainRenderer is mighty tricky. There are a few tricks already used to make rendering as efficient as possible grouping stuff. A lot might also be gained by caching part of the scenes. E.g. the terrain is static as long as the view isn't scrolled or height levels are adjusted. I introduced listeners to make this feasible but again didn't get around using them for this especially due to the existing code complexity

@ololoken
Copy link
Author

ololoken commented Jun 9, 2025

Well, if I had planning a renderer for terrain there must be one big texture atlas with terrain sprites, generated texture with tiles indexes from atlas + gl program which accepts both 2d samples and by pixel copies atlas sprites to it's onscreen positions using mapping from indexes texture. Then in same way apply gl program to render dither between terrain connections water/land greenland/winter etc. Then gl program to render roads, then objects.
Not sure when, but I will try to implement it.

@Flamefire
Copy link
Member

Well, if I had planning a renderer for terrain there must be one big texture atlas with terrain sprites, generated texture with tiles indexes from atlas + gl program which accepts both 2d samples and by pixel copies atlas sprites to it's onscreen positions using mapping from indexes texture. Then in same way apply gl program to render dither between terrain connections water/land greenland/winter etc. Then gl program to render roads, then objects. Not sure when, but I will try to implement it.

We have the texture atlas already. The textures are given as positions/triangles inside that. That's actually inherited from S2.
One major point would also be support for animated textures: Water uses multiple parts of the texture and in some terrains there is e.g. lave doing that too.

If you get some minimal program working just for the terrain textures and transitions that includes the animation I'd be very curious to see that :-)

@Farmer-Markus
Copy link
Contributor

It seems like you have the same problem with the graphics/shadows as my Android port of rttr. Do you know why this happens? Would help me a lot :D

There must something related to textures format/internal format. I'm looking at TerrainRenderer and something tells me that 99% of its code can be done by shaders.

I've fixed the shading issues. I don't know if this works for your web port but here are the changes(just the few in the terrain renderer)

https://github.com/Farmer-Markus/s25rttr-android/blob/main/patch%2Fs25client.patch#L2207

@ololoken
Copy link
Author

@Farmer-Markus awesome, I will try. Currently my only progress is ruining of terrain rendering completely and attempt to show at least something.

@ololoken
Copy link
Author

What I'm working on:

precision highp float;
attribute vec2 aVertexPosition;

uniform mat3 projectionMatrix;

varying vec2 vTextureCoord;

uniform vec4 inputSize;
uniform vec4 outputFrame;

vec4 filterVertexPosition (void) {
    vec2 position = aVertexPosition * max(outputFrame.zw, vec2(0.)) + outputFrame.xy;

    return vec4((projectionMatrix * vec3(position, 1.)).xy, 0., 1.);
}

vec2 filterTextureCoord (void) {
    return aVertexPosition * (outputFrame.zw * inputSize.zw);
}

void main (void) {
    gl_Position = filterVertexPosition();
    vTextureCoord = filterTextureCoord(); // get texture coord
}

and main magic happens in fragment shader

precision highp float;
varying vec2 vTextureCoord;
uniform vec4 outputFrame;
uniform vec4 inputSize;

uniform sampler2D uAtlas; // really big texture with all loaded sprites
uniform vec2 uAtlasSize; // it's size
uniform vec2 uTileSize; // for now supports only fixed tile size
uniform vec2 uOutSize;// "real" output size

uniform sampler2D uMapping; // texture where we store in "zw" vec2 coords of sprite in atlas
uniform vec2 uMappingSize; // mapping texture size
uniform vec2 uBackgroundSize; // background size in map points
uniform vec2 uBackgroundOffset; // background offset in pixels

void main () {
    // copy current pixel color from atlas using mapping texture

    vec2 uv = vTextureCoord/outputFrame.zw*inputSize.xy;//remap to [0..1] as output depends on "inner" tileset
    vec2 tileAtlas = uTileSize/uAtlasSize;
    vec2 screenTile = (uv*uOutSize+uBackgroundOffset)/uTileSize;

    vec2 tileCoord = floor(screenTile); //  index in mapping sample 
    vec2 tileInnerCoord = fract(screenTile)*tileAtlas; // relative coords

    float mappingIndex = tileCoord.y*uBackgroundSize.x+tileCoord.x;
    float mappingY = floor(mappingIndex/uMappingSize.x);
    float mappingX = mappingIndex-mappingY*uMappingSize.x;
    vec2 shapeCoord = vec2(mappingX, mappingY)/uMappingSize;
    // here we finally found x,y of sprite in atlas texture 
    vec2 tileAtlasCoord = 255.0*texture2D(uMapping, shapeCoord).xy*tileAtlas;
    // finally output pixel color using 
    gl_FragColor = texture2D(uAtlas, tileAtlasCoord+tileInnerCoord);
}

nothing really to show yet, but I fill that it might and will work.

@ololoken
Copy link
Author

ololoken commented Jun 24, 2025

image Looks better, runs faster. Will perform some cleanups, add save/load functionality and update PR I think my changes will affect native app performance as well.

@Flamefire
Copy link
Member

Looks better, runs faster. Will perform some cleanups, add save/load functionality and update PR I think my changes will affect native app performance as well.

Great work! I also assume it will improve the native app.

Did you get the animations for water and lava working with the shader?
If so can you factor out the changes to the rendering to a PR separate from the emscripten changes? Besides having more manageable PRs/changesets I'm not fully convinced about emscripten. So for me that would depend on how intrusive the changes required are and of course what @Flow86 thinks about it.

But as mentioned: I'd really love to have a shader based renderer in any case

@Spikeone
Copy link
Member

Looks better, runs faster. Will perform some cleanups, add save/load functionality and update PR I think my changes will affect native app performance as well.

Great work! I also assume it will improve the native app.

Did you get the animations for water and lava working with the shader? If so can you factor out the changes to the rendering to a PR separate from the emscripten changes? Besides having more manageable PRs/changesets I'm not fully convinced about emscripten. So for me that would depend on how intrusive the changes required are and of course what @Flow86 thinks about it.

But as mentioned: I'd really love to have a shader based renderer in any case

I'd love shader based renderer, maybe that'd help improving the lightning and would be closer to the original :)

@ololoken
Copy link
Author

ololoken commented Jun 24, 2025

ok, maybe my update will be a bit disappointing, but I overestimated my dev capabilities to impose such significant refactoring like rendering via gl program.
The good stuff is that I noticed bottleneck in rendering loop while I was trying. And replacement of glDrawArrays with glBegin/glEnd did the magic. Minimum of changes with great result, I like it.

Regarding emscripten, there are a lot of bits to do. It requires lwebsocket.js library patching + you need standalone html/js project to run the build. In other words far from PR candidate.

@Flamefire
Copy link
Member

The good stuff is that I noticed bottleneck in rendering loop while I was trying. And replacement of glDrawArrays with glBegin/glEnd did the magic. Minimum of changes with great result, I like it.

Sounds great! What exactly was that bottleneck?

As for that replacement: I'm actually surprised by that. The immediate mode (glBegin/glEnd) puts more stress on the CPU with the additional driver calls and is deprecated in OpenGL 3 and removed in OpenGL ES and partially(?) in OGL 3.1
I have found reports that glDrawArrays was not faster than immediate mode with explanations that this likely happens in environments doing software rendering, i.e. processing of the OpenGL pipeline is done partially/mostly on CPU so you don't gain much by batching the calls.

Especially as we have users using OpenGL ES we can't switch back to immediate mode.

However having this identified as a bottleneck is still very useful as it means we can improve our usage in this area, e.g. by not transmitting the data on every call but storing it server-side/on GPU.
We should benchmark the difference and see where / which objects benefit most of the change and see how to optimize those using the new API.

@ololoken
Copy link
Author

Well in my case I got performance improvement 5-6 fps (with empty map) -> 45-50fps (very tight road graph) after migration to immediate mode.
Will try to check gl4es library internals, I'm using it as replacement of glad for emscripten build.

@Flamefire
Copy link
Member

I guess we need to check if this is the same for the native builds or if the difference isn't there or opposite of the emscripten build.

Maybe there is some emulation of the OpenGL 3 APIs in the library onto the immediate mode pipeline?

@ololoken
Copy link
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants